/*
 * Copyright (c) 2009.
 */

package kz.home.db;

import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
import javax.microedition.lcdui.*;
import javax.microedition.rms.*;
import java.util.Vector;
import java.io.*;

/**
 * Created by IntelliJ IDEA.
 * User: danik
 * Date: 14.07.2009
 * Time: 1:19:43
 * To change this template use File | Settings | File Templates.
 */
public class PersistentRankingMIDlet extends MIDlet implements CommandListener, RecordListener, Runnable {
    private Command exitCommand;
    private Command okCommand;
    private Command cancelCommand;
    private Command newCommand;
    private Command checkCommand;
    private Command detailsCommand;
    private Command backCommand;
    private Command deleteCommand;
    private Display display;
    private TextField isbnField;
    private StringItem isbnDisplay;
    private StringItem titleDisplay;
    private StringItem rankingDisplay;
    private StringItem reviewDisplay;
    private StringItem checkTitle;
    private Form isbnForm;
    private Form searchForm;
    private Form resultForm;
    private Form checkForm;
    private List bookList;
    private Vector bookInfoList;
    private Thread searchThread;
    private BookStore bookStore;
    private BookInfo searchBookInfo;

    protected void startApp() throws MIDletStateChangeException {
        if (display == null) {
            initialize();

            // If there are any books in the
            // book store, display the list.
            // Otherwise, start with the ISBN form.
            display.setCurrent(getSelectionScreen());
        }
    }

    protected void pauseApp() {
    }

    protected void destroyApp(boolean unconditional) throws MIDletStateChangeException {
        // Close the book store
        if (bookStore != null) {
            try {
                bookStore.removeRecordListener(this);
                bookStore.close();
            } catch (RecordStoreException ex) {
            }
        }
    }

    public void commandAction(Command cmd, Displayable d) {
        if (cmd == exitCommand) {
            try {
                destroyApp(true);
            } catch (MIDletStateChangeException ex) {
            }
            notifyDestroyed();
        } else if (cmd == okCommand) {
            String isbn = isbnField.getString().trim();
            if (!isbn.equals("")) {
                isbnDisplay.setText(isbn);
                display.setCurrent(searchForm);
                searchForBook(new BookInfo(isbn));
            }
        } else if (cmd == cancelCommand) {
            searchThread = null;
            isbnField.setString(null);
            display.setCurrent(getSelectionScreen());
        } else if (cmd == newCommand) {
            isbnField.setString(null);
            display.setCurrent(isbnForm);
        } else if (cmd == detailsCommand || cmd == List.SELECT_COMMAND) {
            int index = bookList.getSelectedIndex();
            searchBookInfo = (BookInfo)bookInfoList.elementAt(index);
            isbnDisplay.setText(searchBookInfo.getIsbn());
            showBookInfo(searchBookInfo);
        } else if (cmd == deleteCommand) {
            int index = bookList.getSelectedIndex();
            BookInfo bookInfo = (BookInfo)bookInfoList.elementAt(index);
            try {
                bookStore.deleteBook(bookInfo);
            } catch (RecordStoreException ex) {
                System.out.println("Delete failed: " + ex);
            }
        } else if (cmd == checkCommand) {
            String isbn = searchBookInfo.getIsbn();
            checkTitle.setText(searchBookInfo.getTitle());
            display.setCurrent(checkForm);
            searchForBook(searchBookInfo);
        } else if (cmd == backCommand) {
            display.setCurrent(getSelectionScreen());
        }
    }

    public void searchForBook(BookInfo info) {
        searchBookInfo = info;
        searchThread = new Thread(this);
        searchThread.start();
    }

    public void recordAdded(RecordStore recordStore, int recordId) {
        // Update the book list
        populateBookList();
    }

    public void recordChanged(RecordStore recordStore, int recordId) {
        // Update the book list
        populateBookList();
    }

    public void recordDeleted(RecordStore recordStore, int recordId) {
        // Update the book list
        populateBookList();
    }

    public void run() {
        try {
            boolean found = true;//Fetcher.fetch(searchBookInfo);
            if (searchThread == Thread.currentThread()) {
                if (found && searchBookInfo.getTitle() != null) {
                    // Display the book details
                    showBookInfo(searchBookInfo);

                    // Add the new book to the book store
                    bookStore.saveBookInfo(searchBookInfo);
                } else {
                    Alert alert = new Alert("Book not found", null, null, AlertType.ERROR);
                    alert.setTimeout(Alert.FOREVER);
                    alert.setString("No book with ISBN " + searchBookInfo.getIsbn() + " was found.");
                    isbnField.setString(null);
                    display.setCurrent(alert, getSelectionScreen());
                }
            }
        } catch (Throwable ex) {
            if (searchThread == Thread.currentThread()) {
                Alert alert = new Alert("Search Failed", null,
                                        null, AlertType.ERROR);
                alert.setTimeout(Alert.FOREVER);
                alert.setString("Search failed:\n" + ex.getMessage());
                isbnField.setString(null);
                display.setCurrent(alert, getSelectionScreen());
            }
        }
    }

    // Shows book details on the result screen
    private void showBookInfo(BookInfo info) {
        titleDisplay.setText(info.getTitle());

        int ranking = info.getRanking();
        int lastRanking = info.getLastRanking();
        int change = ranking - lastRanking;
        String rankingText =
                ranking == 0 ? "" :
                    String.valueOf(ranking);
        if (change > 0) {
            rankingText += ", down by " + change;
        } else if (change < 0) {
            rankingText += ", UP by " + (-change);
        }
        rankingDisplay.setText(rankingText);

        int reviews = info.getReviews();
        int lastReviews = info.getLastReviews();
        change = reviews - lastReviews;
        String reviewText =
            reviews == 0 ? "" :
                    String.valueOf(reviews);
        if (change > 0) {
            reviewText += ", up by " + change;
        }
        reviewDisplay.setText(reviewText);

        display.setCurrent(resultForm);
    }

    // If there are any books in the
    // book store, display the list.
    // Otherwise, start with the ISBN form.
    private Screen getSelectionScreen() {
        return
            bookInfoList.isEmpty() ? (Screen)isbnForm : (Screen)bookList;
    }

    // Populates the list of books
    private void populateBookList() {
        // Clear out any existing content
        int count = bookList.size();
        for (int i = 0; i < count; i++) {
            bookList.delete(0);
        }
        bookInfoList.removeAllElements();

        // Add an entry for each book in the store
        try {
            RecordEnumeration e = bookStore.getBooks();
            while (e.hasNextElement()) {
                int id = e.nextRecordId();
                BookInfo info = bookStore.getBookInfo(id);
                bookInfoList.addElement(info);
                String title = info.getTitle();
                if (title == null || title.equals("")) {
                    title = info.getIsbn();
                }
                bookList.append(title, null);
            }
            e.destroy();
        } catch (Exception ex) {
            // Just leave an empty list.
        }

        // The ISBN list should have an exit command
        // only if the List screen is empty.
        isbnForm.removeCommand(exitCommand);
        if (bookInfoList.isEmpty()) {
            isbnForm.addCommand(exitCommand);
        }
    }

    private void initialize() {
        display = Display.getDisplay(this);

        // Open the book store
        bookStore = new BookStore();

        exitCommand = new Command("Exit", Command.EXIT, 0);
        okCommand = new Command("OK", Command.OK, 0);
        cancelCommand = new Command("Cancel", Command.CANCEL, 0);
        newCommand = new Command("New", Command.SCREEN, 1);
        detailsCommand = new Command("Details", Command.OK, 0);
        checkCommand = new Command("Check", Command.OK, 0);
        backCommand = new Command("Back", Command.CANCEL, 1);
        deleteCommand = new Command("Delete", Command.SCREEN, 1);

        bookList = new List("Books", List.IMPLICIT);
        bookList.addCommand(detailsCommand);
        bookList.addCommand(newCommand);
        bookList.addCommand(deleteCommand);
        bookList.addCommand(exitCommand);
        bookInfoList = new Vector();

        isbnForm = new Form("Book Query");
        isbnForm.append("Enter an ISBN and press OK:");
        isbnField = new TextField("", null, 10, TextField.ANY);
        isbnForm.append(isbnField);
        isbnForm.addCommand(okCommand);
        isbnForm.addCommand(exitCommand);
        isbnForm.addCommand(backCommand);

        searchForm = new Form("Book Search");
        searchForm.append("Searching for ISBN\n");
        isbnDisplay = new StringItem(null, null);
        searchForm.append(isbnDisplay);
        searchForm.append("\nPlease wait....");
        searchForm.addCommand(cancelCommand);

        checkForm = new Form("Details Update");
        checkForm.append("Getting details for\n");
        checkTitle = new StringItem(null, null);
        checkForm.append(checkTitle);
        checkForm.append(" from amazon.com\nPlease wait....");
        checkForm.addCommand(cancelCommand);

        resultForm = new Form("Search Results");
        titleDisplay = new StringItem("Book title: ", null);
        rankingDisplay = new StringItem("Ranking:    ", null);
        reviewDisplay = new StringItem("Reviews:    ", null);
        resultForm.append(titleDisplay);
        resultForm.append(rankingDisplay);
        resultForm.append(reviewDisplay);
        resultForm.addCommand(backCommand);
        resultForm.addCommand(checkCommand);

        // Register for events from all of the forms
        isbnForm.setCommandListener(this);
        searchForm.setCommandListener(this);
        resultForm.setCommandListener(this);
        bookList.setCommandListener(this);

        // Listen for changes in the content of the book store
        bookStore.addRecordListener(this);

        // Install the books held in the record store
        populateBookList();
    }
}

// A class that implements a persistent store
// of books, keyed by ISBN.
class BookStore implements RecordComparator, RecordFilter {
    // The name of the record store used to hold books
    private static final String STORE_NAME = "BookStore";
    // The record store itself
    private RecordStore store;
    // ISBN to be used during a filter operation
    private String searchISBN;


    // Creates a bookstore and opens it
    public BookStore() {
        try {
            store = RecordStore.openRecordStore(STORE_NAME, true);
        } catch (RecordStoreException ex) {
            System.err.println(ex);
        }
    }

    // Closes the bookstore
    public void close() throws RecordStoreException {
        if (store != null) {
            store.closeRecordStore();
        }
    }

    // Gets the number of books in the book store
    public int getBookCount() throws RecordStoreException {
        if (store != null) {
            return store.getNumRecords();
        }
        return 0;
    }

    // Adds a listener to the book store
    public void addRecordListener(RecordListener l) {
        if (store != null) {
            store.addRecordListener(l);
        }
    }

    // Removes a listener from the book store
    public void removeRecordListener(RecordListener l) {
        if (store != null) {
            store.removeRecordListener(l);
        }
    }

    // Gets a sorted list of all of the books in
    // the store.
    public RecordEnumeration getBooks() throws RecordStoreException {
        if (store != null) {
            return store.enumerateRecords(null, this, false);
        }
        return null;
    }

    // Gets a BookInfo from a store record
    // given its ISBN
    public BookInfo getBookInfo(String isbn) throws RecordStoreException, IOException {
        BookInfo bookInfo = null;
        searchISBN = isbn;

        // Look for a book with the given ISBN
        RecordEnumeration e = store.enumerateRecords(
                                        this, null, false);

        // If found, get its identifier and
        // fetch its BookInfo object
        if (e.numRecords() > 0) {
            int id = e.nextRecordId();
            bookInfo = getBookInfo(id);
        }

        // Release the enumeration
        e.destroy();

        return bookInfo;
    }

    // Gets a BookInfo from a store record
    // given its record identifier
    public BookInfo getBookInfo(int id) throws RecordStoreException, IOException {
        byte[] bytes = store.getRecord(id);
        DataInputStream is = new DataInputStream(
                            new ByteArrayInputStream(bytes));

        String isbn = is.readUTF();
        BookInfo info = new BookInfo(isbn);
        info.id = id;
        info.title = is.readUTF();
        info.ranking = is.readInt();
        info.reviews = is.readInt();
        info.lastRanking = is.readInt();
        info.lastReviews = is.readInt();

        return info;
    }

    // Adds an entry to the store or modifies the existing
    // entry if a matching ISBN exists.
    public void saveBookInfo(BookInfo bookInfo) throws IOException, RecordStoreException {
        if (store != null) {
            searchISBN = bookInfo.getIsbn();
            RecordEnumeration e = store.enumerateRecords(
                                        this, null, false);
            if (e.numRecords() > 0) {
                // A matching record exists. Set the id
                // of the BookInfo to match the existing record
                bookInfo.id = e.nextRecordId();
                byte[] bytes = toByteArray(bookInfo);
                store.setRecord(bookInfo.id, bytes, 0, bytes.length);
            } else {
                // Create a new record
                bookInfo.id = store.getNextRecordID();
                byte[] bytes = toByteArray(bookInfo);
                store.addRecord(bytes, 0, bytes.length);
            }

            // Finally, destroy the RecordEnumeration
            e.destroy();
        }
    }

    // Deletes the entry for a book from the store
    public void deleteBook(BookInfo bookInfo) throws RecordStoreException {
        if (store != null) {
            store.deleteRecord(bookInfo.id);
        }
    }

    // RecordComparator implementation
    public int compare(byte[] book1, byte[] book2) {
        try {
            DataInputStream stream1 =
                new DataInputStream(new ByteArrayInputStream(book1));
            DataInputStream stream2 =
                new DataInputStream(new ByteArrayInputStream(book2));

            // Match based on the ISBN, but sort based on the title.
            String isbn1 = stream1.readUTF();
            String isbn2 = stream2.readUTF();
            if (isbn1.equals(isbn2)) {
                return RecordComparator.EQUIVALENT;
            }
            String title1 = stream1.readUTF();
            String title2 = stream2.readUTF();
            int result = title1.compareTo(title2);
            if (result == 0) {
                return RecordComparator.EQUIVALENT;
            }
            return result < 0 ? RecordComparator.PRECEDES :
                                RecordComparator.FOLLOWS;
        } catch (IOException ex) {
            return RecordComparator.EQUIVALENT;
        }
    }

    // RecordFilter implementation
    public boolean matches(byte[] book) {
        if (searchISBN != null) {
            try {
                DataInputStream stream =
                    new DataInputStream(new ByteArrayInputStream(book));

                // Match based on the ISBN.
                return searchISBN.equals(stream.readUTF());
            } catch (IOException ex) {
                System.err.println(ex);
            }
        }

        // Default is not to match
        return false;
    }

    // Writes a record into a byte array.
    private byte[] toByteArray(BookInfo bookInfo) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream os = new DataOutputStream(baos);

        os.writeUTF(bookInfo.isbn);
        os.writeUTF(bookInfo.title == null ? "" : bookInfo.title);
        os.writeInt(bookInfo.ranking);
        os.writeInt(bookInfo.reviews);
        os.writeInt(bookInfo.lastRanking);
        os.writeInt(bookInfo.lastReviews);

        return baos.toByteArray();
    }
}

/**
 * A class that represents a book listing
 * at an online book set, including the number
 * of reviews for the book and its sales ranking.
 */
class BookInfo {
    int    id;          // Used when persisting
    String isbn;        // The book ISBN
    String title;       // The book title
    int    reviews;     // Number of reviews
    int    ranking;     // Current ranking
    int    lastReviews; // Last review count
    int    lastRanking;  // Last ranking

    public BookInfo(String isbn) {
        this.isbn = isbn;
    }

    public String getIsbn() {
        return isbn;
    }

    public String getTitle() {
        return title;
    }

    public int getReviews() {
        return reviews;
    }

    public int getRanking() {
        return ranking;
    }

    public int getLastReviews() {
        return lastReviews;
    }

    public int getLastRanking() {
        return lastRanking;
    }

    // Installs details parsed from an input stream
    public void setFromInputStream(InputStream is) {
        // Use an InputHelper to search the input
        InputHelper helper = new InputHelper(is);
        try {

            // Default new values to current values
            int newRanking = this.ranking;
            int newReviews = this.reviews;

            boolean found = helper.moveAfterString("buying info: ");
            if (!found) {
                return;
            }

            // Gather the title from the rest of this line
            StringBuffer titleBuffer = helper.getRestOfLine();

            // Look for the number of reviews
            found = helper.moveAfterString("Based on ");
            if (!found) {
                return;
            }

            // Gather the number of reviews from the current location
            String reviewString = helper.gatherNumber();

            // Look for the sales rank
            found = helper.moveAfterString("Sales Rank: ");
            if (!found) {
                return;
            }

            // Gather the number from the current location
            String rankingString = helper.gatherNumber();

            // Having safely found everything, set the new title
            title = titleBuffer.toString().trim();

            // Now convert the reviews and ranking to integers.
            // If they fail to convert, just leave the existing
            // values.
            try {
                newRanking = Integer.parseInt(rankingString);
            } catch (NumberFormatException ex) {
            }

            if (newRanking != ranking) {
                lastRanking = ranking;
                ranking = newRanking;
                if (lastRanking == 0) {
                    // First time, set last and current
                    // to the same value
                    lastRanking = ranking;
                }
            }

            try {
                newReviews = Integer.parseInt(reviewString);
            } catch (NumberFormatException ex) {
            }

            if (newReviews != reviews) {
                lastReviews = reviews;
                reviews = newReviews;
                if (lastReviews == 0) {
                    // First time, set last and current
                    // to the same value
                    lastReviews = reviews;
                }
            }
        } catch (IOException ex) {
        } finally {
            // Allow garbage collection
            helper.dispose();
            helper = null;
        }
    }
}

// A class that scans through an input
// stream for strins without reading the
// entire stream into a large string.
class InputHelper {
    // Size of the input buffer
    private static final int BUFFER_SIZE = 1024;
    // The input buffer
    private final char[] buffer = new char[BUFFER_SIZE];
    // Number of characters left in the buffer
    private int charsLeft;
    // Index of the next character in the buffer
    private int nextChar;
    // InputStreamReader used to map to Unicode
    private InputStreamReader reader;


    // Constructs a helper to read a given stream
    public InputHelper(InputStream is) {
        reader = new InputStreamReader(is);
    }

    // Cleans up when no longer needed
    public void dispose() {
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException ex) {
            }
            reader = null;
        }
    }

    // Looks for a given string in the input
    // stream and positions the stream so that the
    // next character read is one beyond the string.
    // Returns true if the string was found, false if
    // not (and the stream will have been completely read).
    public boolean moveAfterString(String str) throws IOException {
        char[] chars = str.toCharArray();
        int count = chars.length;
        char firstChar = chars[0];

        char c = (char)0;
        for (;;) {
            if (c != firstChar && !findNext(firstChar)) {
                // Reached the end of the input stream
                return false;
            }

            boolean mismatch = false;
            for (int i = 1; i < count; i++) {
                c = getNext();
                if (c != chars[i]) {
                    mismatch = true;
                    break;
                }
            }

            if (!mismatch) {
                return true;
            }

            // Mismatch. 'c' has the first mismatched
            // character - start the loop again with
            // that character. This is necessary because we
            // could have found "wweb" while looking for "web"
        }
    }

    // Gets the characters for a number, ignoring
    // the grouping separator. The number starts at the
    // current input position, but any leading non-numerics
    // are skipped.
    public String gatherNumber() throws IOException {
        StringBuffer sb = new StringBuffer();
        boolean gotNumeric = false;
        for (;;) {
            char c = getNext();

            // Skip until we find a digit.
            boolean isDigit = Character.isDigit(c);
            if (!gotNumeric && !isDigit) {
                continue;
            }
            gotNumeric = true;
            if (!isDigit) {
                if (c == '.' || c == ',') {
                    continue;
                }
                break;
            }
            sb.append(c);
        }
        return sb.toString();
    }

    // Gets the balance of the current line
    // and returns it as a StringBuffer
    public StringBuffer getRestOfLine() throws IOException {

        StringBuffer sb = new StringBuffer();
        char c;
        for (;;) {
            c = getNext();
            if (c == '\n' || c == (char)0) {
                break;
            }
            sb.append(c);
        }
        return sb;
    }

    // Gets the next character from the stream,
    // returning (char)0 when all input has been read.
    private char getNext() throws IOException {
        if (charsLeft == 0) {
            charsLeft = reader.read(buffer, 0, BUFFER_SIZE);
            if (charsLeft < 0) {
                return (char)0;
            }
            nextChar = 0;
        }
        charsLeft--;
        return buffer[nextChar++];
    }

    // Finds the next instance of a given character in the
    // input stream. The input stream is positioned after
    // the located character. If EOF is reached without
    // finding the character, false is returned.
    private boolean findNext(char c) throws IOException {
        for (;;) {
            if (charsLeft == 0) {
                charsLeft = reader.read(buffer, 0, BUFFER_SIZE);
                if (charsLeft < 0) {
                    return false;
                }
                nextChar = 0;
            }
            charsLeft--;
            if (c == buffer[nextChar++]) {
                return true;
            }
        }
    }
}
